object.[objectCode].vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. <template>
  2. <div class="object-detail-page">
  3. <header class="object-head">
  4. <n-button text @click="backToSuite">
  5. 返回业务域
  6. </n-button>
  7. <div class="object-title">
  8. <span>
  9. <n-icon><CubeOutline /></n-icon>
  10. </span>
  11. <div>
  12. <h1>{{ object?.objectName || objectCode }}</h1>
  13. <p>{{ object?.description || '业务单元详情' }}</p>
  14. </div>
  15. <DictTag v-if="object" dict-type="sys_enable_disable" :value="object.status" :bordered="false" />
  16. </div>
  17. <div class="object-actions">
  18. <n-button type="primary" @click="openDesigner('form')">
  19. <template #icon>
  20. <n-icon><BuildOutline /></n-icon>
  21. </template>
  22. 设计对象
  23. </n-button>
  24. <n-button secondary :disabled="!object" @click="openObjectEditor">
  25. <template #icon>
  26. <n-icon><CreateOutline /></n-icon>
  27. </template>
  28. 编辑业务单元
  29. </n-button>
  30. <n-button secondary :disabled="!runtimeInfo?.canOpen" @click="openRuntime">
  31. <template #icon>
  32. <n-icon><OpenOutline /></n-icon>
  33. </template>
  34. 打开应用
  35. </n-button>
  36. <n-button secondary @click="scrollToReadiness">
  37. <template #icon>
  38. <n-icon><CheckmarkCircleOutline /></n-icon>
  39. </template>
  40. 查看就绪度
  41. </n-button>
  42. <n-button secondary :disabled="!object" @click="toggleObject">
  43. <template #icon>
  44. <n-icon><PowerOutline /></n-icon>
  45. </template>
  46. {{ object?.status === 1 ? '停用业务单元' : '启用业务单元' }}
  47. </n-button>
  48. <n-button secondary type="error" :disabled="!object" @click="deleteObject">
  49. <template #icon>
  50. <n-icon><TrashOutline /></n-icon>
  51. </template>
  52. 删除业务单元
  53. </n-button>
  54. <n-button v-if="canAdvanced" secondary @click="openDesigner('advanced')">
  55. <template #icon>
  56. <n-icon><SettingsOutline /></n-icon>
  57. </template>
  58. 高级配置
  59. </n-button>
  60. </div>
  61. </header>
  62. <section ref="readinessSectionRef" class="readiness-section">
  63. <ReadinessPanel v-if="object" :object-id="object.id" @action="handleReadinessAction" />
  64. </section>
  65. <section class="operation-guide">
  66. <div class="guide-head">
  67. <div>
  68. <h2>交付路径</h2>
  69. <p>按设计对象、发布对象、运行应用三段检查当前对象,待处理步骤会进入设计器修复。</p>
  70. </div>
  71. </div>
  72. <div class="guide-steps">
  73. <button
  74. v-for="step in operationSteps"
  75. :key="step.key"
  76. type="button"
  77. class="guide-step"
  78. :class="`step-${step.state}`"
  79. @click="step.action()"
  80. >
  81. <span>{{ step.index }}</span>
  82. <div>
  83. <strong>{{ step.title }}</strong>
  84. <small>{{ step.desc }}</small>
  85. </div>
  86. <em>{{ step.stateLabel }}</em>
  87. </button>
  88. </div>
  89. </section>
  90. <section class="runtime-panel">
  91. <n-alert :type="runtimeStatusType" :show-icon="false">
  92. <div class="runtime-alert">
  93. <div>
  94. <strong>{{ runtimeInfo?.message || '正在加载运行态信息' }}</strong>
  95. <p>{{ runtimeHint }}</p>
  96. </div>
  97. <n-space :wrap="true">
  98. <n-button secondary @click="openDesigner('form')">
  99. <template #icon>
  100. <n-icon><BuildOutline /></n-icon>
  101. </template>
  102. 设计对象
  103. </n-button>
  104. <n-button secondary @click="openDesigner('publish')">
  105. <template #icon>
  106. <n-icon><RocketOutline /></n-icon>
  107. </template>
  108. 发布对象
  109. </n-button>
  110. <n-button secondary :disabled="!canUseRuntime" @click="openRuntime">
  111. <template #icon>
  112. <n-icon><OpenOutline /></n-icon>
  113. </template>
  114. 运行应用
  115. </n-button>
  116. <n-button
  117. secondary
  118. :disabled="!canUseImportExport"
  119. :loading="templateLoading"
  120. @click="downloadImportTemplate"
  121. >
  122. <template #icon>
  123. <n-icon><CloudDownloadOutline /></n-icon>
  124. </template>
  125. 导入模板
  126. </n-button>
  127. <n-button secondary :disabled="!canUseImportExport" :loading="importing" @click="triggerImport">
  128. <template #icon>
  129. <n-icon><CloudUploadOutline /></n-icon>
  130. </template>
  131. 导入
  132. </n-button>
  133. <n-button secondary :disabled="!canUseImportExport" :loading="exportLoading" @click="exportData">
  134. <template #icon>
  135. <n-icon><CloudDownloadOutline /></n-icon>
  136. </template>
  137. 导出
  138. </n-button>
  139. </n-space>
  140. </div>
  141. </n-alert>
  142. <input ref="importInputRef" class="hidden-file-input" type="file" accept=".xlsx,.xls" @change="handleImportFile">
  143. </section>
  144. <n-tabs v-model:value="activeTab" type="line" animated class="object-tabs">
  145. <n-tab-pane name="relation" tab="关联数据">
  146. <ObjectRelationPanel :object-id="object?.id" @action="handleReadinessAction" />
  147. </n-tab-pane>
  148. <n-tab-pane name="capability" tab="能力配置">
  149. <BusinessBindingPanel
  150. v-if="object"
  151. target-type="OBJECT"
  152. :target-id="object.id"
  153. :target-code="object.objectCode"
  154. />
  155. </n-tab-pane>
  156. <n-tab-pane v-if="canAdvanced" name="developer" tab="开发者信息">
  157. <div class="developer-info">
  158. <div><span>对象编码</span><code>{{ object?.objectCode || '-' }}</code></div>
  159. <div><span>模型编码</span><code>{{ object?.modelCode || '-' }}</code></div>
  160. <div><span>运行配置</span><code>{{ runtimeInfo?.configKey || '-' }}</code></div>
  161. <div><span>运行路由</span><code>{{ runtimeInfo?.routePath || '-' }}</code></div>
  162. </div>
  163. </n-tab-pane>
  164. </n-tabs>
  165. <BusinessObjectDesignerPage
  166. v-if="designerVisible"
  167. :key="designerMountKey"
  168. embedded
  169. :embedded-object-code="object?.objectCode || objectCode"
  170. :embedded-object-id="object?.id || null"
  171. :embedded-suite-code="suiteCode || ''"
  172. :initial-panel="designerPanel"
  173. @saved="loadObject"
  174. @close="closeDesigner"
  175. />
  176. <BusinessObjectEditorDrawer
  177. v-model:show="objectEditorVisible"
  178. :object="editingObject"
  179. :suites="objectEditorSuites"
  180. @saved="handleObjectEdited"
  181. />
  182. </div>
  183. </template>
  184. <script setup>
  185. import {
  186. BuildOutline,
  187. CheckmarkCircleOutline,
  188. CloudDownloadOutline,
  189. CloudUploadOutline,
  190. CreateOutline,
  191. CubeOutline,
  192. OpenOutline,
  193. PowerOutline,
  194. RocketOutline,
  195. SettingsOutline,
  196. TrashOutline,
  197. } from '@vicons/ionicons5'
  198. import { useMessage } from 'naive-ui'
  199. import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
  200. import { useRoute, useRouter } from 'vue-router'
  201. import {
  202. businessObjectDetail,
  203. businessObjectList,
  204. businessObjectRuntimeInfo,
  205. businessSuiteList,
  206. deleteBusinessObject,
  207. dynamicCrudExport,
  208. dynamicCrudImport,
  209. dynamicCrudImportTemplate,
  210. updateBusinessObjectStatus,
  211. } from '@/api/business-app'
  212. import DictTag from '@/components/DictTag.vue'
  213. import { useTabStore, useUserStore } from '@/store'
  214. import { getDefaultPageTitle } from '@/utils/page-title'
  215. const BusinessBindingPanel = defineAsyncComponent(() => import('./components/BusinessBindingPanel.vue'))
  216. const BusinessObjectEditorDrawer = defineAsyncComponent(() => import('./components/BusinessObjectEditorDrawer.vue'))
  217. const ObjectRelationPanel = defineAsyncComponent(() => import('./components/ObjectRelationPanel.vue'))
  218. const ReadinessPanel = defineAsyncComponent(() => import('./components/ReadinessPanel.vue'))
  219. const BusinessObjectDesignerPage = defineAsyncComponent(() => import('./object-designer.[objectCode].vue'))
  220. const route = useRoute()
  221. const router = useRouter()
  222. const message = useMessage()
  223. const tabStore = useTabStore()
  224. const userStore = useUserStore()
  225. const activeTab = ref('relation')
  226. const object = ref(null)
  227. const runtimeInfo = ref(null)
  228. const importInputRef = ref(null)
  229. const readinessSectionRef = ref(null)
  230. const templateLoading = ref(false)
  231. const importing = ref(false)
  232. const exportLoading = ref(false)
  233. const designerVisible = ref(false)
  234. const designerPanel = ref('form')
  235. const objectEditorVisible = ref(false)
  236. const editingObject = ref(null)
  237. const objectEditorSuites = ref([])
  238. const objectCode = computed(() => route.params.objectCode)
  239. const suiteCode = computed(() => route.query.suiteCode || object.value?.suiteCode)
  240. const pageTitle = computed(() => object.value?.objectName || objectCode.value || '业务单元详情')
  241. const canUseRuntime = computed(() => Boolean(runtimeInfo.value?.canOpen && runtimeInfo.value?.configKey))
  242. const canUseImportExport = computed(() => Boolean(runtimeInfo.value?.configKey))
  243. const runtimeStatusType = computed(() => runtimeInfo.value?.canOpen ? 'success' : 'warning')
  244. const runtimeHint = computed(() => {
  245. if (runtimeInfo.value?.canOpen)
  246. return '对象已发布,可以打开业务应用,也可以继续使用导入、导出等运行能力。'
  247. if (canUseImportExport.value)
  248. return '对象已有运行配置,导入导出可用;运行访问入口仍需完成发布检查。'
  249. if (runtimeInfo.value?.message)
  250. return '先进入对象设计器处理提示中的缺口,再打开业务应用。'
  251. return '请先完成对象设计和发布检查,再生成标准业务访问入口。'
  252. })
  253. const designerMountKey = computed(() => `${object.value?.objectCode || objectCode.value}_${designerPanel.value}`)
  254. const canAdvanced = computed(() => {
  255. return userStore.isAdmin
  256. || hasPermission(userStore.permissions, 'ai:businessObject:advanced')
  257. || hasPermission(userStore.apiPermissions, 'ai:businessObject:advanced')
  258. || hasPermission(userStore.getDataPermission, 'ai:businessObject:advanced')
  259. })
  260. const operationSteps = computed(() => [
  261. {
  262. key: 'design',
  263. index: '01',
  264. title: '设计对象',
  265. desc: '维护对象基础信息、字段、表单、列表和详情。',
  266. state: object.value?.modelCode ? 'done' : 'todo',
  267. stateLabel: object.value?.modelCode ? '已设计' : '去设计',
  268. action: () => openDesigner('form'),
  269. },
  270. {
  271. key: 'page',
  272. index: '02',
  273. title: '页面编排',
  274. desc: '检查列表、表单和详情页是否已生成。',
  275. state: runtimeInfo.value?.configKey ? 'done' : 'todo',
  276. stateLabel: runtimeInfo.value?.configKey ? '有配置' : '去搭建',
  277. action: () => openDesigner('form'),
  278. },
  279. {
  280. key: 'publish',
  281. index: '03',
  282. title: '发布对象',
  283. desc: '执行发布检查并生成可运行访问入口。',
  284. state: runtimeInfo.value?.canOpen ? 'done' : 'todo',
  285. stateLabel: runtimeInfo.value?.canOpen ? '已发布' : '去发布',
  286. action: () => openDesigner('publish'),
  287. },
  288. {
  289. key: 'runtime',
  290. index: '04',
  291. title: '运行应用',
  292. desc: '打开列表、导入、导出并验证数据。',
  293. state: canUseRuntime.value ? 'done' : 'locked',
  294. stateLabel: canUseRuntime.value ? '可打开' : '待就绪',
  295. action: canUseRuntime.value ? openRuntime : () => openDesigner('publish'),
  296. },
  297. ])
  298. onMounted(loadObject)
  299. watch(pageTitle, (title) => {
  300. if (!title)
  301. return
  302. route.meta.title = title
  303. document.title = `${title} | ${getDefaultPageTitle()}`
  304. tabStore.updateTabTitle(route.fullPath, title)
  305. }, { immediate: true })
  306. async function loadObject() {
  307. const res = await businessObjectList({
  308. suiteCode: suiteCode.value,
  309. objectCode: objectCode.value,
  310. })
  311. object.value = (res.data || [])[0] || null
  312. if (object.value) {
  313. const runtimeRes = await businessObjectRuntimeInfo(object.value.id)
  314. runtimeInfo.value = runtimeRes.data || null
  315. }
  316. }
  317. function backToSuite() {
  318. if (suiteCode.value)
  319. router.push(`/app-center/suite/${suiteCode.value}`)
  320. else
  321. router.push('/app-center')
  322. }
  323. function openRuntime() {
  324. if (!runtimeInfo.value?.canOpen) {
  325. message.warning(runtimeInfo.value?.message || '应用暂不可打开')
  326. return
  327. }
  328. router.push(runtimeInfo.value.routePath)
  329. }
  330. function openDesigner(panel = 'form') {
  331. const code = object.value?.objectCode || objectCode.value
  332. if (!code)
  333. return
  334. designerPanel.value = panel || 'form'
  335. designerVisible.value = true
  336. }
  337. async function closeDesigner() {
  338. designerVisible.value = false
  339. await loadObject()
  340. }
  341. async function openObjectEditor() {
  342. if (!object.value?.id)
  343. return
  344. await loadObjectEditorSuites()
  345. try {
  346. const res = await businessObjectDetail(object.value.id)
  347. editingObject.value = { ...object.value, ...(res.data || {}) }
  348. }
  349. catch {
  350. editingObject.value = { ...object.value }
  351. }
  352. objectEditorVisible.value = true
  353. }
  354. async function loadObjectEditorSuites() {
  355. if (objectEditorSuites.value.length)
  356. return
  357. const res = await businessSuiteList()
  358. objectEditorSuites.value = res.data || []
  359. }
  360. async function handleObjectEdited(payload) {
  361. if (payload?.suiteCode && payload.suiteCode !== suiteCode.value) {
  362. await router.replace({
  363. path: `/app-center/object/${payload.objectCode || objectCode.value}`,
  364. query: { ...route.query, suiteCode: payload.suiteCode },
  365. })
  366. }
  367. await loadObject()
  368. }
  369. function scrollToReadiness() {
  370. readinessSectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  371. }
  372. function handleReadinessAction(action) {
  373. switch (action) {
  374. case 'CREATE_OBJECT':
  375. case 'ENABLE_OBJECT':
  376. // 刷新页面数据
  377. loadObject()
  378. break
  379. case 'CREATE_APP_ENTRY':
  380. case 'ENABLE_APP_ENTRY':
  381. openDesigner('publish')
  382. break
  383. case 'CONFIGURE_MODEL':
  384. openDesigner('fields')
  385. break
  386. case 'CONFIGURE_RUNTIME':
  387. case 'PUBLISH_APP':
  388. case 'ENABLE_RUNTIME':
  389. openDesigner('publish')
  390. break
  391. case 'OPEN_RUNTIME':
  392. openRuntime()
  393. break
  394. case 'CONFIGURE_RELATIONS':
  395. openDesigner('relations')
  396. break
  397. case 'CONFIGURE_BINDINGS':
  398. activeTab.value = 'capability'
  399. break
  400. default:
  401. break
  402. }
  403. }
  404. async function applyObjectStatus(currentObject, nextStatus) {
  405. await updateBusinessObjectStatus(currentObject.id, nextStatus)
  406. message.success(nextStatus === 0 ? '业务单元已停用' : '业务单元已启用')
  407. await loadObject()
  408. }
  409. async function toggleObject() {
  410. if (!object.value)
  411. return
  412. const currentObject = object.value
  413. const nextStatus = currentObject.status === 1 ? 0 : 1
  414. if (nextStatus === 0) {
  415. window.$dialog?.warning({
  416. title: '停用业务单元',
  417. content: `确定停用“${currentObject.objectName || currentObject.objectCode}”吗?停用后关联配置仍保留,但用户不应继续进入该业务单元办理新业务。`,
  418. positiveText: '停用',
  419. negativeText: '取消',
  420. onPositiveClick: () => applyObjectStatus(currentObject, nextStatus),
  421. })
  422. return
  423. }
  424. await applyObjectStatus(currentObject, nextStatus)
  425. }
  426. function deleteObject() {
  427. if (!object.value)
  428. return
  429. window.$dialog?.warning({
  430. title: '删除业务单元',
  431. content: `确定删除“${object.value.objectName || object.value.objectCode}”吗?已关联关系或访问入口的对象会被后端拦截。`,
  432. positiveText: '删除',
  433. negativeText: '取消',
  434. onPositiveClick: async () => {
  435. await deleteBusinessObject(object.value.id)
  436. message.success('业务单元已删除')
  437. backToSuite()
  438. },
  439. })
  440. }
  441. async function downloadImportTemplate() {
  442. if (!canUseImportExport.value) {
  443. message.warning('缺少运行配置,无法下载导入模板')
  444. return
  445. }
  446. templateLoading.value = true
  447. try {
  448. const response = await dynamicCrudImportTemplate(runtimeInfo.value.configKey)
  449. downloadBlobResponse(response, `${object.value?.objectName || objectCode.value}-导入模板.xlsx`)
  450. }
  451. finally {
  452. templateLoading.value = false
  453. }
  454. }
  455. function triggerImport() {
  456. if (!canUseImportExport.value) {
  457. message.warning('缺少运行配置,无法导入')
  458. return
  459. }
  460. importInputRef.value?.click()
  461. }
  462. async function handleImportFile(event) {
  463. const file = event.target.files?.[0]
  464. event.target.value = ''
  465. if (!file)
  466. return
  467. importing.value = true
  468. try {
  469. const res = await dynamicCrudImport(runtimeInfo.value.configKey, file)
  470. const result = res.data || {}
  471. if (result.success === false) {
  472. message.error(result.summary || '导入失败')
  473. return
  474. }
  475. message.success(result.summary || '导入完成')
  476. }
  477. finally {
  478. importing.value = false
  479. }
  480. }
  481. async function exportData() {
  482. if (!canUseImportExport.value) {
  483. message.warning('缺少运行配置,无法导出')
  484. return
  485. }
  486. exportLoading.value = true
  487. try {
  488. const response = await dynamicCrudExport(runtimeInfo.value.configKey, {})
  489. if (response?.data?.async) {
  490. message.success(response.data.message || '导出任务已提交')
  491. return
  492. }
  493. downloadBlobResponse(response, `${object.value?.objectName || objectCode.value}-导出数据.xlsx`)
  494. message.success('导出成功')
  495. }
  496. finally {
  497. exportLoading.value = false
  498. }
  499. }
  500. function hasPermission(source, permission) {
  501. if (!Array.isArray(source))
  502. return false
  503. return source.includes(permission) || source.includes('**') || source.includes('*:*:*')
  504. }
  505. function downloadBlobResponse(response, fallbackName) {
  506. const blob = response?.data instanceof Blob ? response.data : response
  507. if (!(blob instanceof Blob)) {
  508. throw new TypeError('下载响应不是文件流')
  509. }
  510. const fileName = resolveDownloadFileName(response, fallbackName)
  511. const url = URL.createObjectURL(blob)
  512. const link = document.createElement('a')
  513. link.href = url
  514. link.download = fileName
  515. document.body.appendChild(link)
  516. link.click()
  517. document.body.removeChild(link)
  518. URL.revokeObjectURL(url)
  519. }
  520. function resolveDownloadFileName(response, fallbackName) {
  521. const disposition = response?.headers?.['content-disposition']
  522. || response?.headers?.get?.('content-disposition')
  523. || ''
  524. const utf8Match = disposition.match(/filename\*=utf-8''([^;]+)/i)
  525. if (utf8Match?.[1])
  526. return decodeURIComponent(utf8Match[1])
  527. const normalMatch = disposition.match(/filename="?([^";]+)"?/i)
  528. if (normalMatch?.[1])
  529. return decodeURIComponent(normalMatch[1])
  530. return fallbackName
  531. }
  532. </script>
  533. <style scoped>
  534. .object-detail-page {
  535. min-height: 100%;
  536. background: #f6f8fb;
  537. padding: 20px;
  538. }
  539. .object-head,
  540. .runtime-panel,
  541. .operation-guide,
  542. .readiness-section,
  543. .object-tabs {
  544. border: 1px solid #e5e7eb;
  545. border-radius: 8px;
  546. background: #fff;
  547. }
  548. .object-head {
  549. margin-bottom: 16px;
  550. padding: 16px;
  551. }
  552. .runtime-panel {
  553. margin-bottom: 16px;
  554. padding: 12px;
  555. }
  556. .operation-guide {
  557. margin-bottom: 16px;
  558. padding: 16px;
  559. }
  560. .readiness-section {
  561. margin-bottom: 16px;
  562. }
  563. .guide-head {
  564. display: flex;
  565. justify-content: space-between;
  566. gap: 12px;
  567. margin-bottom: 12px;
  568. }
  569. .guide-head h2 {
  570. margin: 0;
  571. color: #111827;
  572. font-size: 16px;
  573. letter-spacing: 0;
  574. }
  575. .guide-head p {
  576. margin: 5px 0 0;
  577. color: #64748b;
  578. font-size: 12px;
  579. line-height: 1.5;
  580. }
  581. .guide-steps {
  582. display: grid;
  583. grid-template-columns: repeat(4, minmax(0, 1fr));
  584. gap: 10px;
  585. }
  586. .guide-step {
  587. display: grid;
  588. grid-template-columns: 36px minmax(0, 1fr);
  589. gap: 10px;
  590. align-items: start;
  591. min-width: 0;
  592. border: 1px solid #e5e7eb;
  593. border-radius: 8px;
  594. background: #fff;
  595. padding: 12px;
  596. cursor: pointer;
  597. text-align: left;
  598. transition:
  599. border-color 0.2s,
  600. box-shadow 0.2s,
  601. transform 0.2s;
  602. }
  603. .guide-step:hover {
  604. border-color: #93c5fd;
  605. box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06);
  606. transform: translateY(-1px);
  607. }
  608. .guide-step > span {
  609. display: flex;
  610. align-items: center;
  611. justify-content: center;
  612. width: 36px;
  613. height: 36px;
  614. border-radius: 8px;
  615. background: #eff6ff;
  616. color: #2563eb;
  617. font-size: 12px;
  618. font-weight: 700;
  619. }
  620. .guide-step.step-done > span {
  621. background: #dcfce7;
  622. color: #16a34a;
  623. }
  624. .guide-step.step-locked > span {
  625. background: #f1f5f9;
  626. color: #64748b;
  627. }
  628. .guide-step strong,
  629. .guide-step small,
  630. .guide-step em {
  631. display: block;
  632. }
  633. .guide-step strong {
  634. overflow: hidden;
  635. color: #111827;
  636. font-size: 13px;
  637. text-overflow: ellipsis;
  638. white-space: nowrap;
  639. }
  640. .guide-step small {
  641. margin-top: 4px;
  642. color: #64748b;
  643. font-size: 12px;
  644. line-height: 1.4;
  645. }
  646. .guide-step em {
  647. grid-column: 2;
  648. margin-top: 8px;
  649. color: #2563eb;
  650. font-size: 12px;
  651. font-style: normal;
  652. font-weight: 600;
  653. }
  654. .guide-step.step-done em {
  655. color: #16a34a;
  656. }
  657. .guide-step.step-locked em {
  658. color: #64748b;
  659. }
  660. .runtime-alert {
  661. display: flex;
  662. justify-content: space-between;
  663. gap: 16px;
  664. align-items: center;
  665. }
  666. .runtime-alert strong,
  667. .runtime-alert p {
  668. display: block;
  669. }
  670. .runtime-alert strong {
  671. color: #111827;
  672. font-size: 14px;
  673. }
  674. .runtime-alert p {
  675. margin: 6px 0 0;
  676. color: #6b7280;
  677. font-size: 12px;
  678. line-height: 1.5;
  679. }
  680. .hidden-file-input {
  681. display: none;
  682. }
  683. .object-title {
  684. display: grid;
  685. grid-template-columns: 48px minmax(0, 1fr) auto;
  686. gap: 14px;
  687. align-items: center;
  688. margin-top: 12px;
  689. }
  690. .object-title > span {
  691. display: grid;
  692. width: 48px;
  693. height: 48px;
  694. place-items: center;
  695. border-radius: 8px;
  696. background: #eef6ff;
  697. color: #2563eb;
  698. font-size: 24px;
  699. }
  700. .object-title h1 {
  701. margin: 0;
  702. color: #111827;
  703. font-size: 24px;
  704. letter-spacing: 0;
  705. }
  706. .object-title p {
  707. margin: 6px 0 0;
  708. color: #6b7280;
  709. font-size: 13px;
  710. }
  711. .object-actions {
  712. display: flex;
  713. gap: 10px;
  714. margin-top: 16px;
  715. }
  716. .object-tabs {
  717. padding: 8px 16px 16px;
  718. }
  719. .placeholder-grid {
  720. display: grid;
  721. grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  722. gap: 12px;
  723. }
  724. .placeholder-item,
  725. .developer-info > div {
  726. border: 1px solid #e5e7eb;
  727. border-radius: 8px;
  728. background: #fff;
  729. padding: 14px;
  730. }
  731. .placeholder-item strong {
  732. color: #111827;
  733. font-size: 14px;
  734. }
  735. .placeholder-item p {
  736. margin: 8px 0 0;
  737. color: #6b7280;
  738. font-size: 13px;
  739. line-height: 1.5;
  740. }
  741. .developer-info {
  742. display: grid;
  743. gap: 10px;
  744. }
  745. .developer-info > div {
  746. display: grid;
  747. grid-template-columns: 100px minmax(0, 1fr);
  748. gap: 10px;
  749. align-items: center;
  750. }
  751. .developer-info span {
  752. color: #6b7280;
  753. font-size: 13px;
  754. }
  755. .developer-info code {
  756. overflow-wrap: anywhere;
  757. color: #111827;
  758. font-size: 13px;
  759. }
  760. @media (max-width: 620px) {
  761. .object-detail-page {
  762. padding: 12px;
  763. }
  764. .object-title {
  765. grid-template-columns: 42px minmax(0, 1fr);
  766. }
  767. .object-title :deep(.n-tag) {
  768. grid-column: 2;
  769. justify-self: start;
  770. }
  771. .object-actions {
  772. flex-direction: column;
  773. }
  774. .guide-steps {
  775. grid-template-columns: 1fr;
  776. }
  777. .runtime-alert {
  778. align-items: stretch;
  779. flex-direction: column;
  780. }
  781. }
  782. </style>